Notification Center
Notification Center
Intro
El Notification Center (NC) busca centralizar comunicaciones al usuario.
Se basa en eventos que se comunican por diversos canales. Cada evento tiene un suceso en la plataforma que lo dispara, datos asociados y una lógica que determina a quienes notificar y cómo.
Los canales de notificación o Notificators implementados hasta ahora son 2:
- Server Sent Events (SSE)
- Inbox
Cada evento tiene su event_name y su event_payload.
El cliente debe implementar una lógica para cada evento,
ya que el payload y procesamiento de los distintos eventos es distinto.
Por ejemplo, un evento de lote compartido incluirá un field_id para indicar el lote
y el cliente deberá recargar los lotes y mostrárselo al usuario.
Por otro lado un evento de nueva imagen deberá indicar el field_id, layer_name y layer_date
y el cliente deberá recargar las imágenes de ese lote y mostrar esa imagen en el selector de fechas.
Stackable events
Hay eventos que por su frecuencia de ocurrencia, se deben apilar o agrupar. Por ejemplo, imágenes nuevas. Si un usuario tiene 50 lotes cercanos, cuando el satélite pase por estos, se dispararán 50 eventos simultáneos, inundando los canales de notificacioń. Entonces se busca apilarlos para enviar una única notificación.
Cada evento tiene su lógica para apilar. Por ejemplo, apilar todo lo que ocurra el mismo día, o todo lo que ocurra el mismo día para un determinado lote.
Esto además cambia el contenido de las notificaciones ya que en el ejemplo de las imágenes, se enviará el payload de los 50 eventos y el mensaje a mostrar en vez de ser "Tienes una nueva imagen" será "Hay 50 nuevas imágenes".
Expiración
Cada tipo de evento tiene definido un intervalo de expiración. Cada evento tiene una fecha de expiración. Cuando se produce un evento, se registra como fecha de expiración el momento de creación + el intervalo de expiración. Cuando se obtenga el listado de eventos, sólo se obtendrán aquellos que no estén expirados en el momento de la consulta.
Inbox
Es el listado de eventos de un usuario.
Cada item del Inbox representa un evento o varios eventos apilados. Cada item tiene:
event_namestr: nombre del evento.messagestr: mensaje ya traducido para mostrar al usuario.payloadobject: objeto con el payload del item. Cada evento formatea distinto su payload, por lo que hay que ver la especificación de cada uno para implementarlo.timestampstr: fecha del último evento en formatoYYYY-MM-DD"T"HH24:MI:ssZ.dissmisiblebool: indica si se puede eliminar el item.pinnedbool: indica que el item deberá ser destacado en el inbox. Los items pinneados se deberán mostrar primeros en la lista.item_idstr: uuid del item.
SSE
De manera asyncrona se suscribe a un usuario a un topico de redis. Cada topico recibe el nombre de canal y cada evento que utilice el notificador por SSE debe especificar un canal. La idea es que cada canal agrupe eventos asociados a un mismo tema, y los clientes se suscriban solo a los canales que les interesen.
Funcionamiento del Código
El NC consiste de una API HTTP y un worker de celery.
Los eventos que se disparan desde los distintos servicios los recibe el worker con la funcion
source.service.events.event_trigger.
El evento se crea a partir de la informacion que recibe y ejecuta su logica.
Ahi define a quien se debe notificar (EventTarget),
el contenido del evento (EventPayload) y los canales de notificacion.
En el caso del Inbox, tambien define si es un evento agrupable y su condicion de agrupamiento.
La API HTTP tiene:
- un endpoint de event-stream para el notificador SSE
- deja a un usuario escuchando multiples topicos de redis. Los eventos que invocan al notificador del SSE publican mensajes en estos topicos.
- endpoints asociados al inbox
- consulta en la BD los items del inbox y los datos del evento.
Cada evento consiste en:
- un
event_nameunico que sirve de codigo para identificar el evento. - un schema de creacion que define los datos que debe enviar cada servicio, necesarios para crear el evento.
- Los datos del schema son atributos de la instancia del evento (se definen en el init). Notar que esto no es el payload
- una clase asociada que herede de
EventPayload, que defina el payload del evento.- Si el evento se guarda en la BD, su payload se guarda. Este debe incluir todos los datos necesarios para rearmar la notificacion.
- Ademas, el
EventPayloaddefine como se formatea el payload que se entrega al cliente, en caso que no se quieran entregar todos los datos (_format_single_payload_message). - Tambien puede definir deep o soft links
- Si el evento es agrupable, se debe definir una clase asociada que herede de
EventStackedPayloadpara formatear los payload de los eventos de cara al cliente (_format_stacked_payload_message).
- una clase asociada que herede de
EventMessage, que defina los mensajes a mostrar al usuario.- el mensaje que se define es un template, que se traducira.
- el template se formateara con el contenido del
EventPayload - Si el evento es agrupable, se debe definir tambien un mensaje para el
EventStackedPayload
- una fecha de expiracion
expires_at. Cada evento tienen un intervalo de expiracion definido en la BD, y por defecto se expirara pasado ese tiempo desde su creacion. - un metodo
load- toma los datos del evento y realiza lo necesario para definir los targets y el payload.
- opcionalmente, guarda el evento en la BD (necesario si se implementa el
InboxNotificator) - define los notificadores:
- En caso del SSE, solo necesita indicar el canal
- En caso del Inbox, configura si es agrupable y su condicion, si es borrable y su duracion.
Nuevo evento
Para crear un evento:
- Crear la definición del evento en
source.events.models.NEW_EVENT.py- Crear el payload asociado, heredando de
EventPayload.- Definir el
__init__con los**kwargsque forman el payload - Definir el
_format_single_payload_message, si aplica, para entregar al cliente. Por defecto, entrega todos los atributos.
- Definir el
- Si aplica, crear el payload apilado asociado, heredando de
StackedEventPayload.- Definir el
_format_stacked_payload_message, si aplica, para entregar al cliente, que agrupe los payloads de la manera deseada. Por defecto entrega el primerEventPayload._format_single_payload_messagedel array si sólo hay uno, o un diccionario con la keypayloady un array deEventPayload._format_single_payload_messagepor cada payload del array.
- Definir el
- Crear el mensaje asociado, heredando de
EventMessage.- Definir el
message_template, formateable con los attributos delEventPayloaddefinido. - Si aplica, definir el
stacked_message_template, formateable con los attributos del payload. - Si es necesario, definir un nuevo
format_for_client. Este método, según el payload recibido, formateará y traducirá los mensajes. Por defecto, si se trata de unEventPayloado de unStackedEventPayloadde largo 1, utilizará elmessage_templateformateado con los atributos del payload. Si se trata de unStackedEventPayloadde largo > 1, formateará elstacked_message_templatecon el parámetron_valuescuyo valor es el largo del array de payloads.
- Definir el
- Creal la clase del evento, heredando de
AuraEvent:- definir el
event_name - definir el
creation_schema, en formatojsonschema, con los parámetros necesarios para crear un evento. - definir el
__init__(**kwargs)que recibirá los atributos delcreation_schemay defina los atributos del evento. - asignarle los
payload_class,stacked_payload_classymessage_class - definir el método
load. Este es el método principal y debe (en este orden):- definir
self.payload: instancia de laEventPayloaddefinida para el evento - guardar en evento en la base de datos
- definir
self.targets: array deEventTargetcon los destinos a recibir las notificaciones. Si la cantidad de destinos puede ser muy grande, utilizar las clases de iteradores para reducir la carga de memoria. - definir
self.notificators: array de notificadores, inicializados con sus configuraciones.
- definir
- definir el
- Agregarlo al registro de eventos con el decorador
@EventFactory.register() - Agregar el
event_nameen la Base de Datos, tablaevent_definitions
- Crear el payload asociado, heredando de
- Crear el canal SSE (si aplica)
- En
source.notificators.sse.channels.NEW_CHANNEL.py, herdando deSSEChannel - Definirle
namecon el nombre del canal. - Definirle el
redis_queue_templatecon el nombre del tópico - Definir el método
get_redis_queueque formateeredis_queue_templatecon los atributos de unEventTarget - Agregarlo al registro de canales con el decorador
@ChannelFactory.register() - El front debera contemplar este canal
- En
Traducciones
Las traducciones se encuentran en translations/files, en los archivos .po de cada lenguaje.
Las traducciones se realizan con los siguientes pasos:
- Marcar los textos a traducir.
en caso que el texto se defina en algún lugar donde el
localeno está disponible, marcarlo así:luego donde se deba traducir:from core.translations import translatable
s = translatable('texto a traducir')en caso que el texto esé definido en el lugar donde está definido elfrom core.translations import TRANSLATOR
TRANSLATOR.gettext(variable_con_texto, locale)locale:from core.translations import TRANSLATOR
TRANSLATOR.gettext('texto a traducir', locale) - correr el script para actualizar los archivos. Asegurarse tener Babel en el entorno.
source translations/update_translations - Traducir los
.po - correr el script para generar los binarios
.mosource translations/update_binaries
El
requirements.dev.txtincluye las dependencias para realizar las traducciones.